import { $, Argv, SessionError, Time, Universal, h } from "koishi"

export const name = "minecraft-bridge"
export const inject = {
  required: ["database"],
  optional: ["autoDeleteResponse"],
}

const config = {
  mPlatform: "minecraft.xdi8aho",
  mChannelId: "0",
  mCommandMode: "whitelist",
  mCommands: {
    bind: true,

  },
  lPlatform: "onebot",
  lSelfId: "2748080608",
  lGuildId: "754732657",
  lChannelId: "754732657",
  lUseCensor: true,
  hidePlayers: [
    "ServerOverflow1",
    "Cornbread2100_",
    "TypicalModMaker",
    "ServerSeekerV2",
    "dc_sysadmin_best",
  ],
}

export function apply(ctx) {
  //
  // localization
  //
  ctx.i18n.define("zh-CN", {
    commands: {
      mc: {
        description: "MC 服务器相关指令",
        ping: {
          description: "查看当前在线玩家列表和服务器 TPS MSPT",
        },
        player: {
          description: "假人指令",
        },
        say: {
          description: "向游戏内聊天转发消息",
        },
      },
    },
    "minecraft-bridge": {
      "minecraft-bot-disconnected": "无法联系 MCC 客户端…",
      "minecraft-bot-offline": "MC bot 似乎已离线（已 <i18n:time value={0}/>未接收到游戏时间更新）…",
      "timeout": "请求超时…",
      "invalid-name": "【无法识别玩家名】",
      "player-joined": "{0} 加入了游戏。",
      "player-left": "{0} 退出了游戏。",
    },
    permissions: {
      "minecraft-bridge": {
        "bound-player": "绑定玩家",
      },
    },
  })

  //
  // identify bots
  //
  const mCtx = ctx.platform(config.mPlatform).channel(config.mChannelId)
  const lCtx = ctx.platform(config.lPlatform).self(config.lSelfId).channel(config.lChannelId)    .union(ctx.platform("qq"))
  let mBot = null
  let lBot = null
  const findLBot = () => {
    lBot = ctx.bots[`${config.lPlatform}:${config.lSelfId}`] ?? null
    ctx.logger.debug("lBot", lBot?.status)
  }
  const testMBot = async bot => {
    if (bot.platform === config.mPlatform) {
      for await (const guild of bot.getGuildIter()) {
        if (guild.id === config.mChannelId) {
          ctx.logger.debug("mSelfId", bot.selfId)
          mBot = bot
          return true
        }
      }
    }
    return false
  }
  const findMBot = async () => {
    while (true) {
      for (const bot of ctx.bots)
        if (mBot || await testMBot(bot).catch(e => { ctx.logger.warn("find mBot", e) })) return
      await ctx.sleep(60000)
    }
  }
  ctx.on("ready", () => {
    findLBot()
    findMBot()
  })()
  const up = ({ bot }) => {
    findLBot()
    if (!mBot && bot.status === Universal.Status.ONLINE) testMBot(bot).catch(e => ctx.logger.warn("test mBot", e))
  }
  ctx.on("login-added", up)
  ctx.on("login-updated", up)
  ctx.on("login-removed", ({ bot }) => {
    findLBot()
    if (bot.sid === mBot.sid) {
      mBot = null
      ctx.logger.debug("clear mBot")
      findMBot()
    }
  })

  //
  // m manual quotes
  //
  /*mCtx.middleware((session, next) => {
    ctx.logger.debug("m prepend", session)
    // TODO: 手动引用语法…
    return next()
  }, true)*/

  //
  // m command rewrite
  //
  /*mCtx.on("before-attach", session => {
    try {
    session.argv ??= Argv.parse(session.stripped.content)
    ctx.logger.debug(Object.fromEntries(Object.entries(session.argv).filter(([k,v])=>k!="session")))
    if (!mCtx.$commander.inferCommand(session.argv)) return
    const commandName = session.argv.command.name
    const resolution = Object.hasOwn(config.mCommands, commandName) ? config.mCommands[commandName] : config.mCommandMode !== "whitelist"
    if (resolution === false) {
      session.argv = { session, tokens: [] }
    } else if (typeof resolution === "string") {
      delete session.argv.command
      session.argv.name = resolution
    }
    } catch (e) { ctx.logger.error(e) }
  })*/

  //
  // utility: m player and l user names
  //
  function mPlayerName(session, name) {
    return session.event.user.name ?? h.parse(session.text("minecraft-bridge.invalid-player-name"))
  }
  async function lUserNick(session) {
    return (
      session.user?.name ||
      session.event?.member?.nick ||
      //(m => m?.nick || m?.user?.nick)(await session.bot.getGuildMember(session.guildId, session.userId).catch(() => null)) ||
      session.event?.user?.nick ||
      session.event?.user?.name ||
      session.userId
    )
  }

  //
  // utility: l send
  //
  async function lSend(...args) {
    if (!lBot) return ctx.logger.warn("lSend: no lBot")
    const { locales } = await ctx.database.get("channel", { platform: config.lPlatform, id: config.lChannelId }, ["locales"])
    const session = lBot.session({
      type: "session",
      channel: { id: config.lChannelId, type: Universal.Channel.Type.TEXT },
      guild: { id: config.lGuildId },
    })
    session.locales = locales
    return session.send(...args)
  }

  //
  // m player join/leave announcements
  //
  const leaveMap = Object.create(null)
  let counter = 0
  mCtx.on("guild-member-added", async (session) => {
    // https://github.com/koishijs/koishi/issues/1470
    if (session.platform !== "minecraft") return
    if (session.userId === session.selfId) return
    ctx.logger.debug("joined", session.event.user.name, session.mcc.data.uuid)
    if (config.hidePlayers.includes(session.event.user.name)) return
    if (
      session.mcc?.data?.uuid &&
      leaveMap[session.mcc.data.uuid] &&
      leaveMap[session.mcc.data.uuid].counter >= counter - 3 &&
      leaveMap[session.mcc.data.uuid].time >= Date.now() - 5 * Time.minute
    ) {
      try {
        for (const m of await leaveMap[session.mcc.data.uuid].msgs)
          await lBot.deleteMessage(config.lChannelId, m)
        return
      } catch {
        // nop
      } finally {
        delete leaveMap[session.mcc.data.uuid]
      }
    }
    lSend(session.text("minecraft-bridge.player-joined", [h("censor", mPlayerName(session))]))
    counter++
  })
  mCtx.on("guild-member-removed", async (session) => {
    if (session.platform !== "minecraft") return
    if (session.userId === session.selfId) return
    ctx.logger.debug("left", session.event.user.name, session.mcc.data.uuid)
    if (config.hidePlayers.includes(session.event.user.name)) return
      const msgs = lSend(session.text("minecraft-bridge.player-left", [h("censor", mPlayerName(session))]))
    leaveMap[session.mcc.data.uuid] = { time: Date.now(), msgs, counter: counter++ }
  })
  lCtx.on("message", () => { counter++ })

  //
  // forward messages
  //
  ctx.before("attach-user", (session, fields) => {
    if (/*mCtx.filter(session) ||*/ lCtx.filter(session)) fields.add("name")
  })
  mCtx.middleware((session, next) => {
    if (!session.content.match(/^[：:]/)) return next()
    lSend(`<censor>${session.user?.name || mPlayerName(session)}: ${session.content.slice(1).trimStart()}</censor>`)
  })
  lCtx.middleware(async (session, next) => {
    if (!session.content.match(/^[：:]/)) return next()
    mBot.sendMessage(config.mChannelId, `${await lUserNick(session)}: ${session.content.slice(1).trimStart()}`)
  })

  //
  // process m heartbeats
  //
  //const tpsRecord = []
  //const maxTpsRecords = 6
  //let tpsRecordSum = 0
  let lastTimeUpdate = Date.now()
  let worldAge = NaN
  let gameTime = NaN
  mCtx.on("mcc/time-update", async (session) => {
    const now = Date.now()
    //const ticks = session.mcc.data.worldAge - worldAge
    //const milliseconds = now - lastTimeUpdate
    //const tps = ticks / (milliseconds / 1000)
    //if (Number.isFinite(tps)) {
    //  tpsRecord.push(tps)
    //  tpsRecordSum += tps
    //  if (tpsRecord.length > maxTpsRecords) tpsRecordSum -= tpsRecord.shift()
    //}
    //ctx.logger.debug((tps, tpsRecordSum - Math.max(...tpsRecord) - Math.min(...tpsRecord)) / (tpsRecord.length - 2))
    lastTimeUpdate = Date.now()
    worldAge = session.mcc.data.worldAge
    gameTime = session.mcc.data.timeOfDay
  })
  function isOnline() {
    if (Number.isNaN(worldAge)) {
      ctx.logger.debug("isOnline() worldAge is NaN")
      return false
    }
    ctx.logger.debug("isOnline()", "lastTimeUpdate", lastTimeUpdate)
    return Date.now() - lastTimeUpdate < 10000
  }
  function getTimeStrings() {
    const worldAgeDays = ~~(worldAge / 20 / 60 / 60 / 24)
    const worldAgeHours = ~~(worldAge / 20 / 60 / 60) % 24
    const worldAgeMins = ~~(worldAge / 20 / 60) % 60
    const worldAgeSecs = ~~(worldAge / 20) % 60
    const gameTimeDay = ~~(gameTime / 24000 + 0.25) + 1
    const gameTimeHour = ~~(gameTime / 1000 + 6) % 24
    const gameTimeMin = ~~(gameTime * 0.06) % 60
    return {
      worldAgeIRL: `${worldAgeDays}d ${worldAgeHours}h ${worldAgeMins}m ${worldAgeSecs}s IRL`,
      gameDateTime: `D${gameTimeDay} ${String(gameTimeHour).padStart(2, "0")}:${String(gameTimeMin).padStart(2, "0")}`,
    }
  }

  //
  // l bound player usergroup
  //
  lCtx.permissions.provide("minecraft-bridge:bound-player", async (name, session) => {
    return session.user?.id != null && !!(await ctx.database.select("binding", { platform: "minecraft", aid: session.user.id }).execute(r => $.count(r)))
  })

  //
  // utility: get tps
  //
  {
    let cbs = new Set()
    function getTPS() {
      const p = new Promise((resolve, reject) => {
        if (!mBot) reject(new SessionError(["minecraft-bridge.minecraft-bot-disconnected"]))
        if (!isOnline()) reject(new SessionError(["minecraft-bridge.minecraft-bot-offline"], [Date.now() - lastTimeUpdate]))
        const timeout = setTimeout(() => reject(new SessionError(["minecraft-bridge.timeout"])), 15000)
        cbs.add(val => {
          clearTimeout(timeout)
          resolve(val)
        })
        mBot.internal._send("/send /forge tps")
      })
      return p
    }

    mCtx.on("mcc/chat-raw", async (session) => {
      if (!cbs.size) return
      try {
        const j = JSON.parse(session.mcc.data.json)
        if (j.translate === "commands.forge.tps.summary.all") {
          for (const cb of cbs) cb([+j.with[1], +j.with[0]])
          cbs.clear()
        }
      } catch (e) {
        ctx.logger.warn("forge tps", e)
      }
    })
  }

  //
  // l parent command
  //
  lCtx.command("mc")

  //
  // l command: mc ping
  //
  lCtx.command("mc.ping", { checkArgCount: true, checkUnknown: true }).action(async ({ session }) => {
    /*if (!mBot) return session.text("minecraft-bridge.minecraft-bot-disconnected")
    if (!isOnline()) return session.text("minecraft-bridge.minecraft-bot-offline", [Date.now() - lastTimeUpdate])*/
    const [tps, mspt] = (await getTPS()).map(n => (+n).toFixed(1))
    const { compare } = new Intl.Collator("en")
    const colors = [
      "color:#55FF55;--text-shadow-color:#153F15",
      "color:#FFFF55;--text-shadow-color:#3F3F15",
      "color:#FFAA00;--text-shadow-color:#3E2A00",
      "color:#FF5555;--text-shadow-color:#3F1515"
    ]
    const { worldAgeIRL, gameDateTime } = getTimeStrings()
    const result = `
      <html style="font:16px 'Minecraft Seven v2',Unifont;font-kerning:none;color:white;background:black;text-align:center;padding:.125em">
        <table style="border-collapse:collapse;margin:auto">
          ${Object.entries(await mBot.internal.getPlayersLatency())
            .sort(([a], [b]) => compare(a, b))
            .map(([n, p]) => `
              <tr>
                <td>${n}</td>
                <td style="padding-left:2em;text-align:right;${colors[p<=150?0:p<=300?1:p<=500?2:3]};text-shadow:var(--text-shadow-color) .125em .125em">${~~p}ms</td>
              </tr>
            `).join("")}
        </table>
        <div>
            <span style="color:#AAA;text-shadow:#2A2A2A .125em .125em">TPS:</span
            > <span style="${colors[tps>=20?0:tps>=19.5?1:tps>=18?2:3]};text-shadow:var(--text-shadow-color) .125em .125em">${tps}</span
            > <span style="color:#AAA;text-shadow:#2A2A2A .125em .125em">MSPT:</span
            > <span style="${colors[mspt<=35?0:mspt<=42.5?1:mspt<=50?2:3]};text-shadow:var(--text-shadow-color) .125em .125em">${mspt}</span>
        </div>
        <div style="font-size:.5em;color:#AAA;text-shadow:#2A2A2A .125em .125em"
          >WorldAge: ${worldAge} (${worldAgeIRL})<br
          >InGameTime: ${gameTime} (${gameDateTime})</div>
      </html>
    `.replace(/\n\s*/g, "")
    if (ctx.autoDeleteResponse) return ctx.autoDeleteResponse.send(session, result)
    return result
  })

  //
  // l command: mc player
  //
  lCtx.command("mc.player <args:text>", {
    permissions: ["minecraft-bridge:bound-player"],
    strictOptions: true,
  }).action(async ({ session }, args) => {
    mBot.internal._send("/send /player " + args)
  })

  //
  // l command: mc say
  //
  lCtx.command("mc.say <text:rawtext>", { strictOptions: true }).action(async ({ session }, text) => {
    await mBot.sendMessage(config.mChannelId, `${await lUserNick(session)}: ${text}`)
  })

  //
  // m command: time
  //
  mCtx.command("time").action(() => {
    const { worldAgeIRL, gameDateTime } = getTimeStrings()
    return h.text(`WorldAge=${worldAge} (${worldAgeIRL}), InGameTime=${gameTime} (${gameDateTime})`)
  })

  /*mCtx.on("mcc/chat-raw", async (session) => {
    ctx.logger.debug(session.mcc.data.json)
  })*/
}
